package com.vision.sales.sv.serv;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.jasperreports.engine.JRDataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import com.vision.core.cm.db.data.Company;
import com.vision.core.cm.exception.RpcException;
import com.vision.core.sv.db.CustomSessionFactoryBean;
import com.vision.core.sv.db.mgr.CompanyManager;
import com.vision.core.sv.db.mgr.DataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.serv.BaseRpcServlet;
import com.vision.core.sv.serv.util.ServletUtils;
import com.vision.core.sv.util.CollectionUtils;
import com.vision.sales.cm.data.Month;
import com.vision.sales.cm.data.SalesPermissions;
import com.vision.sales.cm.data.TransactionStatus;
import com.vision.sales.cm.db.data.CollectionItem;
import com.vision.sales.cm.db.data.CreditMemo;
import com.vision.sales.cm.db.data.Customer;
import com.vision.sales.cm.db.data.SalesInvoice;
import com.vision.sales.sv.db.mgr.CollectionItemManager;
import com.vision.sales.sv.db.mgr.CreditMemoDataManager;
import com.vision.sales.sv.db.mgr.CustomerManager;
import com.vision.sales.sv.db.mgr.SalesInvoiceManager;
import com.vision.sales.sv.report.AbstractDataSource;
import com.vision.sales.sv.report.CustomerSalesRow;
import com.vision.sales.sv.report.Report;
import com.vision.sales.sv.report.ReportWriter;

/**
 * Servlet for printing Customer Sales report.
 * 
 * This servlet is accessible via the /print/customer_sales path in the URL (see web.xml)
 * 
 * GET parameters:
 * <ul>
 * 	<li>title - the title that will appear in the report
 * 	<li>type - 'M' for monthly, 'A' annual. If parameter is unparseable, this defaults to 'A'.
 * 	<li>months - the months to include, separated by commas (ex: 1,2,3 for Jan to March). Applies when type is M.
 * 	<li>year - the year to include (ex: 2012). Applies when type is either M or A.
 * 	<li>companies - optional comma-separated list of company ids indicating which companies to include. If not specified, all companies will be included.</li>
 * 	<li>customers - optional comma-separated list of customer ids indicating which customers to include. If not specified, all customers will be included.</li>
 * </ul>
 * 
 * Example:
 * <pre>/print/customer_sales?title=Customer%20Sales&type=A&year=2012&companies=1,2&customers=5,10,22&types=0,1,2</pre>
 * <pre>/print/customer_sales?title=Customer%20Sales&type=M&months=1,2,3&companies=1,2&customers=5,10,22&types=0,1,2</pre>
 * 
 * @author Mark
 */
@Service("print_customer_sales")
public class PrintCustomerSalesServlet extends BaseRpcServlet {

	private static final long serialVersionUID = 1L;
	
	private static final Logger log = LoggerFactory.getLogger(PrintCustomerSalesServlet.class);

	@Autowired
	@Qualifier("companyManager")
	private DataManager<Company> companyManager;
	
	@Autowired
	@Qualifier("customerManager")
	private DataManager<Customer> customerManager;
	
	@Autowired
	@Qualifier("salesInvoiceManager")
	private DataManager<SalesInvoice> invoiceManager;

	@Autowired
	@Qualifier("collectionItemManager")
	private DataManager<CollectionItem> collectionItemManager;

	@Autowired
	@Qualifier("creditMemoManager")
	private DataManager<CreditMemo> creditMemoManager;

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		try {
			ServletUtils.checkPermissions(SalesPermissions.ViewCustomerSalesReport);
		} catch (RpcException e) {
			resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You are not authorized to perform this operation.");
			return;
		} 
		
		// Parameters --
		log.debug("Parsing parameters from URL...");
		CustomerSalesParams params = new CustomerSalesParams(req);
		if (params.error != null) {
			resp.sendError(HttpServletResponse.SC_BAD_REQUEST, params.error);
			return;
		}
		
		try {
			// Build the Report
			log.debug("Building report...");
			Report report = new Report();
			String title = params.title == null || params.title == "" ? 
					"Customer Sales Report" : params.title;
			String monthYear = "";
			if (params.isAnnual()){
				monthYear = title + " " + params.year;
				
			} else {
				StringBuilder str = new StringBuilder();
				for (int month : params.months) {
					str.append(Month.MONTHS[month-1] + " ");
				}
				monthYear = title + " for " + str.toString() + params.year;
			}
			report.setFilename(monthYear);
			report.setTitle(monthYear);
			report.setSubtitle(getCompanyDetails(params.companyIds));
			report.setSize(params.size);
			report.setOrientation(params.orientation);

			// Columns and Grouping
			report.addStringColumn("customer", "Customer", 60, null);

			if (!params.isInternal()) {
				report.addStringColumn("address", "Address", 60, null);
				report.addStringColumn("tin", "TIN", 60, null);
			}
			
			if (params.isAnnual()) {
				// add all the months in column
				for (String m : Month.MONTHS) {
					report.addDoubleColumn(m, m, 30, null);
				}
				report.addDoubleColumn("total", "Total", 40, null);
			} else {
				// add gross, discounts, net columns
				for (String c : CustomerSalesRow.COLUMNS) {
					report.addDoubleColumn(c, c, 30, null);
				}
			}
			
			// Data Source
			log.debug("Creating data source...");
			report.setDataSource(getDataSource(params));
			
			// Print and Write to Stream
			log.debug("Writing report to stream...");
			ReportWriter.write(report, resp);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "The server was unable to process the request.");
		}
	}
		
	private JRDataSource getDataSource(CustomerSalesParams params) {
		// get data from the database
		List<Customer> customers = getCustomers(params);
		List<CollectionItem> collections = getCollectionItems(params);

		// build rows
		List<CustomerSalesRow> rows =
				params.isAnnual() 
				? getAnnualRows(customers, collections, params)
				: getMonthlyRows(customers, params);
		
		JRDataSource ds = new AbstractDataSource<CustomerSalesRow>(rows) {
			@Override
			public Object getValue(CustomerSalesRow row, String fieldName) {
				return row.get(fieldName);
			}
		};
		
		return ds;
	}

	private List<CustomerSalesRow> getAnnualRows(
			List<Customer> customers,
			List<CollectionItem> collections, 
			CustomerSalesParams params) {

		List<CustomerSalesRow> rows = addCustomerRows(customers, params);
		addEmptyRow(rows, 2);
		addCollectionRows(rows, collections);
		
		for (CustomerSalesRow row : rows)
			row.computeAnnualTotal();
				
		return rows;
	}

	private List<CustomerSalesRow> getMonthlyRows(
			List<Customer> customers,
			CustomerSalesParams params) {

		List<CustomerSalesRow> rows = addCustomerRowsWithCreditMemos(customers, params);
		
		for (CustomerSalesRow row : rows)
			row.computeMonthlyTotal();
				
		return rows;
	}

	private List<CustomerSalesRow> addCustomerRows(
			List<Customer> customers,
			CustomerSalesParams params) {

		// create row for each customer
		Map<Long, CustomerSalesRow> map = new HashMap<Long, CustomerSalesRow>();
		List<Long> customerIds = new ArrayList<Long>();
		for (Customer customer : customers) {
			map.put(customer.getId(), new CustomerSalesRow(customer, params.isInternal()));
			customerIds.add(customer.getId());
		}

		// if there's no selected customer ids, include them in the parameter to filter credit memos 
		if (params.customerIds == null){
			params.customerIds = customerIds;
		}

		List<SalesInvoice> invoices = getInvoices(params);
		
		// create row for totals
		CustomerSalesRow invoicesTotalRow = new CustomerSalesRow("Total");
		
		// fill rows with data
		for (SalesInvoice invoice : invoices) {
			CustomerSalesRow customerRow = map.get(invoice.getCustomer().getId());
			if (customerRow == null) {
				log.warn("CustomerSalesRow was not found.");
				continue;
			}
			Date date = invoice.getDate();
			@SuppressWarnings("deprecation")
			String mo = Month.MONTHS[date.getMonth()];

			fillCustomerSalesRow(customerRow, mo, invoice.getGrossSales());
			fillCustomerSalesRow(invoicesTotalRow, mo, invoice.getGrossSales());
		}

		// combine the customer rows and the total row into a list
		List<CustomerSalesRow> rows = new ArrayList<CustomerSalesRow>(map.values()); 
		Collections.sort(rows);
		rows.add(invoicesTotalRow);
		
		return rows;
	}

	@SuppressWarnings("deprecation")
	private List<CustomerSalesRow> addCustomerRowsWithCreditMemos(
			List<Customer> customers, 
			CustomerSalesParams params) {
		
		// create row for each customer
		Map<Long, CustomerSalesRow> map = new HashMap<Long, CustomerSalesRow>();
		List<Long> customerIds = new ArrayList<Long>();
		for (Customer customer : customers) {
			map.put(customer.getId(), new CustomerSalesRow(customer, params.isInternal()));
			customerIds.add(customer.getId());
		}

		// if there's no selected customer ids, include them in the parameter to filter credit memos 
		if (params.customerIds == null){
			params.customerIds = customerIds;
		}

		List<SalesInvoice> invoices = getInvoices(params);
		List<CreditMemo> creditMemos = getCreditMemos(params);

		// create row for totals
		CustomerSalesRow invoicesTotalRow = new CustomerSalesRow("Total");
		
		// fill rows with gross sales of invoice
		for (SalesInvoice invoice : invoices) {
			int month = invoice.getDate().getMonth() + 1;
			if (!params.months.contains(month))
				continue;
			
			Long customerId = invoice.getCustomer().getId();
			CustomerSalesRow customerRow = map.get(customerId);
			if (customerRow == null) {
				log.warn("CustomerSalesRow was not found.");
				continue;
			}
			fillCustomerSalesRow(customerRow, CustomerSalesRow.GROSS_SALES, invoice.getGrossSales());
			fillCustomerSalesRow(invoicesTotalRow, CustomerSalesRow.GROSS_SALES, invoice.getGrossSales());
		}

		// fill rows with collections
		for (CreditMemo creditMemo: creditMemos) {
			int month = creditMemo.getDate().getMonth() + 1;
			if (!params.months.contains(month))
				continue;
			
			Long customerId = creditMemo.getCustomer().getId();
			CustomerSalesRow customerRow = map.get(customerId);
			if (customerRow == null) {
				log.warn("CustomerSalesRow was not found.");
				continue;
			}
			
			// note: credit memo regardless of type
			fillCustomerSalesRow(customerRow, CustomerSalesRow.DISCOUNTS, creditMemo.getAmount());
			fillCustomerSalesRow(invoicesTotalRow, CustomerSalesRow.DISCOUNTS, creditMemo.getAmount());
		}
		
		// combine the customer rows and the total row into a list
		List<CustomerSalesRow> rows = new ArrayList<CustomerSalesRow>(map.values()); 
		Collections.sort(rows);
		rows.add(invoicesTotalRow);
		
		return rows;
	}

	@SuppressWarnings("deprecation")
	private void addCollectionRows(List<CustomerSalesRow> rows, List<CollectionItem> collections) {
		
		// create rows for collection totals
		CustomerSalesRow gross = new CustomerSalesRow("Gross Sales");
		CustomerSalesRow overruns = new CustomerSalesRow("Overruns");
		CustomerSalesRow overpayments = new CustomerSalesRow("Overpayments");
		CustomerSalesRow discounts = new CustomerSalesRow("Discounts");
		CustomerSalesRow rejects = new CustomerSalesRow("Rejects");
		CustomerSalesRow returns = new CustomerSalesRow("Returns");
		CustomerSalesRow badDebts = new CustomerSalesRow("Bad Debts");
		CustomerSalesRow subtotal = new CustomerSalesRow("Subtotal");
		CustomerSalesRow tax = new CustomerSalesRow("Tax");
		CustomerSalesRow netAmount= new CustomerSalesRow("Net Amount");
		
		for (CollectionItem collection : collections) {
			String mo = Month.MONTHS[collection.getDate().getMonth()];
			
			fillCustomerSalesRow(gross, mo, collection.getAmount());
			fillCustomerSalesRow(overruns, mo, collection.getOverruns());
			fillCustomerSalesRow(overpayments, mo, collection.getOverpayments());
			fillCustomerSalesRow(discounts, mo, collection.getDiscounts());
			fillCustomerSalesRow(rejects, mo, collection.getRejects());
			fillCustomerSalesRow(returns, mo, collection.getReturns());
			fillCustomerSalesRow(subtotal, mo, collection.getSubtotal());
			fillCustomerSalesRow(tax, mo, collection.getTax());
			fillCustomerSalesRow(netAmount, mo, collection.getNetAmount());
		}
		
		rows.add(gross);
		rows.add(overruns);
		rows.add(overpayments);
		rows.add(discounts);
		rows.add(rejects);
		rows.add(returns);
		rows.add(badDebts);
		rows.add(subtotal);
		rows.add(tax);
		rows.add(netAmount);
	}

	private void addEmptyRow(List<CustomerSalesRow> rows, int numberOfRows) {
		CustomerSalesRow emptyRow = new CustomerSalesRow("");
		for (String mo : Month.MONTHS)
			emptyRow.getSalesMap().put(mo, 0d); // Initialize sales to zero

		for (int i = 0; i < numberOfRows; i++) {
			rows.add(emptyRow);
		}
	}

	void fillCustomerSalesRow(CustomerSalesRow row, String column, Double amount) {
		if (amount == null) amount = 0d;
		
		Double value = row.getSalesMap().get(column);
		if (value == null)
			row.getSalesMap().put(column, amount);
		else
			row.getSalesMap().put(column, value + amount);
	}
	
	// -------------------- Database queries -----------------------------

	private String getCompanyDetails(List<Long> companyIds) {
		if (companyIds == null || companyIds.isEmpty())
			return "All Companies";
			
		companyManager = new CompanyManager();
		companyManager.setSessionFactory(CustomSessionFactoryBean.getFactory());
		
		log.debug("Retrieving company...");
		StringBuilder companyNames = new StringBuilder();
		for (Long companyId : companyIds) {

			Company company = companyManager.getById(companyId);
			if (company != null)
			{
				companyNames.append(company.getCode() + " - " + company.getName());	
			}
		}
		
		return companyNames.toString();
	}

	protected List<Customer> getCustomers(CustomerSalesParams params) {
		// XXX @Autowired doesn't seem to work, thus this patch
		customerManager = new CustomerManager();
		customerManager.setSessionFactory(CustomSessionFactoryBean.getFactory());
		
		log.debug("Retrieving customers...");
		List<Customer> customers = null;
		if (params.customerIds == null || params.customerIds.isEmpty())
			customers = customerManager.getAll();
		else
			customers = customerManager.getByIds("id", params.customerIds);
		
		return customers;
	}

	@SuppressWarnings("unchecked")
	protected List<SalesInvoice> getInvoices(CustomerSalesParams params) {
		// XXX @Autowired doesn't seem to work, thus this patch
		invoiceManager = new SalesInvoiceManager();
		invoiceManager.setSessionFactory(CustomSessionFactoryBean.getFactory());
		
		ISimpleCriteria crit = SimpleCriteria.forClass(SalesInvoice.class);
		crit.eq("status", TransactionStatus.POSTED);
		
		crit.add(DateExtract.year("date", params.year));
		
		if (!params.isAnnual())
			crit.getDetached().add(DateExtract.in("MONTH", "date", params.months));
		
		if (params.companyIds != null && !params.companyIds.isEmpty())
			crit.in("company.id", params.companyIds);
		
		if (params.customerIds != null && !params.customerIds.isEmpty())
			crit.in("customer.id", params.customerIds);
		
		crit.asc("date");
		
		log.debug("Retrieving sales invoices...");
		List<SalesInvoice> invoices = invoiceManager.getTemplate().findByCriteria(crit.getDetached());
		return invoices;
	}
	
	@SuppressWarnings("unchecked")
	protected List<CollectionItem> getCollectionItems(CustomerSalesParams params) {
		// XXX @Autowired doesn't seem to work, thus this patch
		collectionItemManager = new CollectionItemManager();
		collectionItemManager.setSessionFactory(CustomSessionFactoryBean.getFactory());
		
		ISimpleCriteria crit = SimpleCriteria.forClass(CollectionItem.class);
		// TODO Get collections first then get CollectionItem
//		crit.eq("collection.status", TransactionStatus.POSTED);
		
		crit.add(DateExtract.year("date", params.year));
		
		if (!params.isAnnual())
			crit.add(DateExtract.in("MONTH", "date", params.months));
		
		if (params.companyIds != null && !params.companyIds.isEmpty())
			crit.in("company.id", params.companyIds);
		
		if (params.customerIds != null && !params.customerIds.isEmpty())
			crit.in("customer.id", params.customerIds);
		
		crit.asc("date");
		
		log.debug("Retrieving collection items...");
		List<CollectionItem> items = collectionItemManager.getTemplate().findByCriteria(crit.getDetached());
		return items;
	}
	
	@SuppressWarnings("unchecked")
	protected List<CreditMemo> getCreditMemos(CustomerSalesParams params) {
		// XXX @Autowired doesn't seem to work, thus this patch
		creditMemoManager = new CreditMemoDataManager();
		creditMemoManager.setSessionFactory(CustomSessionFactoryBean.getFactory());

		ISimpleCriteria crit = SimpleCriteria.forClass(CreditMemo.class);
		
		crit.add(DateExtract.year("date", params.year));
		if (!params.isAnnual())
			crit.add(DateExtract.in("MONTH", "date", params.months));
		
		if (params.companyIds != null && !params.companyIds.isEmpty())
			crit.in("company.id", params.companyIds);
		
		if (params.customerIds != null && !params.customerIds.isEmpty())
			crit.in("customer.id", params.customerIds);
		
		crit.asc("date");
		
		log.debug("Retrieving credit memo...");
		List<CreditMemo> creditMemos = creditMemoManager.getTemplate().findByCriteria(crit.getDetached());
		return creditMemos;
	}
	
	
	// Inner classes %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	static class CustomerSalesParams extends PeriodSalesReportParams {
		
		String visibility;
		
		public CustomerSalesParams(HttpServletRequest req) {
			super(req);
			
			setDefaultTitle("Customer Sales Report");

			String sCompanyIds = req.getParameter("company");
			if (sCompanyIds != null)
				companyIds = CollectionUtils.longsFromString(sCompanyIds, ',');
			
			
			visibility = req.getParameter("visibility");
			if (!"I".equalsIgnoreCase(visibility) && !"E".equalsIgnoreCase(visibility)) {
				log.warn("Visibility cannot be determined. Defaulting to internal.");
				visibility = "I";
			}
		}
		
		public boolean isInternal() {
			return "I".equalsIgnoreCase(visibility);
		}
	}
}
